/********************************************************************************************************
 * PROGRAM	  : 
 * DATE - TIME  : lundi 10 avril 2006 - 22:28
 * AUTHOR	   : Darren Hiebert and Anacr0x ( fred.julian at gmail.com )
 * FILENAME	 : 
 * LICENSE	  : GPL
 * COMMENTARY   : Modified file (with qt) of the ctags project
 ********************************************************************************************************/

/*
*   This module contains functions for reading tag files.
*/

/*
*   INCLUDE FILES
*/
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>		/* to declare off_t */

#include "readtags.h"

/*
*   MACROS
*/
#define TAB '\t'


/*
*   DATA DECLARATIONS
*/
typedef struct
{
	size_t size;
	char *buffer;
}
vstring;

/* Information about current tag file */
struct sTagFile
{
	/* has the file been opened and this structure initialized? */
	short initialized;
	/* format of tag file */
	short format;
	/* how is the tag file sorted? */
	sortType sortMethod;
	/* pointer to file structure */
	FILE *fp;
	/* file position of first character of `line' */
	off_t pos;
	/* size of tag file in seekable positions */
	off_t size;
	/* last line read */
	vstring line;
	/* name of tag in last line read */
	vstring name;
	/* defines tag search state */
	struct
	{
		/* file position of last match for tag */
		off_t pos;
		/* name of tag last searched for */
		const char *name;
		/* length of name for partial matches */
		size_t nameLength;
		/* peforming partial match */
		short partial;
		/* ignoring case */
		short ignorecase;
	}
	search;
	/* miscellaneous extension fields */
	struct
	{
		/* number of entries in `list' */
		unsigned short max;
		/* list of key value pairs */
		tagExtensionField *list;
	}
	fields;
	/* buffers to be freed at close */
	struct
	{
		/* name of program author */
		char *author;
		/* name of program */
		char *name;
		/* URL of distribution */
		char *url;
		/* program version */
		char *version;
	}
	program;
};

/*
*   DATA DEFINITIONS
*/
const char *const EmptyString = "";
const char *const PseudoTagPrefix = "!_";

/*
*   FUNCTION DEFINITIONS
*/

/*
 * Compare two strings, ignoring case.
 * Return 0 for match, < 0 for smaller, > 0 for bigger
 * Make sure case is folded to uppercase in comparison (like for 'sort -f')
 * This makes a difference when one of the chars lies between upper and lower
 * ie. one of the chars [ \ ] ^ _ ` for ascii. (The '_' in particular !)
 */
static int struppercmp(const char *s1, const char *s2)
{
	int result;
	do
	{
		result = toupper((int) *s1) - toupper((int) *s2);
	}
	while (result == 0 && *s1++ != '\0' && *s2++ != '\0');
	return result;
}

static int strnuppercmp(const char *s1, const char *s2, size_t n)
{
	int result;
	do
	{
		result = toupper((int) *s1) - toupper((int) *s2);
	}
	while (result == 0 && --n > 0 && *s1++ != '\0' && *s2++ != '\0');
	return result;
}

static int growString(vstring * s)
{
	int result = 0;
	size_t newLength;
	char *newLine;
	if (s->size == 0)
	{
		newLength = 128;
		newLine = (char *) malloc(newLength);
		*newLine = '\0';
	}
	else
	{
		newLength = 2 * s->size;
		newLine = (char *) realloc(s->buffer, newLength);
	}
	if (newLine == NULL)
		perror("string too large");
	else
	{
		s->buffer = newLine;
		s->size = newLength;
		result = 1;
	}
	return result;
}

/* Copy name of tag out of tag line */
static void copyName(tagFile * const file)
{
	size_t length;
	const char *end = strchr(file->line.buffer, '\t');
	if (end == NULL)
	{
		end = strchr(file->line.buffer, '\n');
		if (end == NULL)
			end = strchr(file->line.buffer, '\r');
	}
	if (end != NULL)
		length = end - file->line.buffer;
	else
		length = strlen(file->line.buffer);
	while (length >= file->name.size)
		growString(&file->name);
	strncpy(file->name.buffer, file->line.buffer, length);
	file->name.buffer[length] = '\0';
}

static int readTagLineRaw(tagFile * const file)
{
	int result = 1;
	int reReadLine;

	/*  If reading the line places any character other than a null or a
	 *  newline at the last character position in the buffer (one less than
	 *  the buffer size), then we must resize the buffer and reattempt to read
	 *  the line.
	 */
	do
	{
		char *const pLastChar = file->line.buffer + file->line.size - 2;
		char *line;

		file->pos = ftell(file->fp);
		reReadLine = 0;
		*pLastChar = '\0';
		line = fgets(file->line.buffer, (int) file->line.size, file->fp);
		if (line == NULL)
		{
			/* read error */
			if (!feof(file->fp))
				perror("readTagLine");
			result = 0;
		}
		else if (*pLastChar != '\0' && *pLastChar != '\n' && *pLastChar != '\r')
		{
			/*  buffer overflow */
			growString(&file->line);
			fseek(file->fp, file->pos, SEEK_SET);
			reReadLine = 1;
		}
		else
		{
			size_t i = strlen(file->line.buffer);
			while (i > 0 &&
			        (file->line.buffer[i - 1] == '\n'
			         || file->line.buffer[i - 1] == '\r'))
			{
				file->line.buffer[i - 1] = '\0';
				--i;
			}
		}
	}
	while (reReadLine && result);
	if (result)
		copyName(file);
	return result;
}

static int readTagLine(tagFile * const file)
{
	int result;
	do
	{
		result = readTagLineRaw(file);
	}
	while (result && *file->name.buffer == '\0');
	return result;
}

static tagResult growFields(tagFile * const file)
{
	tagResult result = TagFailure;
	unsigned short newCount = 2 * file->fields.max;
	tagExtensionField *newFields = (tagExtensionField *)
	                               realloc(file->fields.list, newCount * sizeof(tagExtensionField));
	if (newFields == NULL)
		perror("too many extension fields");
	else
	{
		file->fields.list = newFields;
		file->fields.max = newCount;
		result = TagSuccess;
	}
	return result;
}

static void parseExtensionFields(tagFile * const file, tagEntry * const entry,
                                 char *const string)
{
	char *p = string;
	while (p != NULL && *p != '\0')
	{
		while (*p == TAB)
			*p++ = '\0';
		if (*p != '\0')
		{
			char *colon;
			char *field = p;
			p = strchr(p, TAB);
			if (p != NULL)
				*p++ = '\0';
			colon = strchr(field, ':');
			if (colon == NULL)
				entry->kind = field;
			else
			{
				const char *key = field;
				const char *value = colon + 1;
				*colon = '\0';
				if (strcmp(key, "kind") == 0)
					entry->kind = value;
				else if (strcmp(key, "file") == 0)
					entry->fileScope = 1;
				else if (strcmp(key, "line") == 0)
					entry->address.lineNumber = atol(value);
				else
				{
					if (entry->fields.count == file->fields.max)
						growFields(file);
					file->fields.list[entry->fields.count].key = key;
					file->fields.list[entry->fields.count].value = value;
					++entry->fields.count;
				}
			}
		}
	}
}

static void parseTagLine(tagFile * file, tagEntry * const entry)
{
	int i;
	char *p = file->line.buffer;
	char *tab = strchr(p, TAB);
	int fieldsPresent = 0;

	entry->fields.list = NULL;
	entry->fields.count = 0;
	entry->kind = NULL;
	entry->fileScope = 0;

	entry->name = p;
	if (tab != NULL)
	{
		*tab = '\0';
		p = tab + 1;
		entry->file = p;
		tab = strchr(p, TAB);
		if (tab != NULL)
		{
			*tab = '\0';
			p = tab + 1;
			if (*p == '/' || *p == '?')
			{
				/* parse pattern */
				int delimiter = *(unsigned char *) p;
				entry->address.lineNumber = 0;
				entry->address.pattern = p;

				do
				{
					p = strchr(p + 1, delimiter);
				}
				while (p != NULL && *(p - 1) == '\\');
				if (p == NULL)
				{
					/* invalid pattern */
				}
				else
					++p;
			}
			else if (isdigit((int) *(unsigned char *) p))
			{
				/* parse line number */
				entry->address.pattern = p;
				entry->address.lineNumber = atol(p);
				while (isdigit((int) *(unsigned char *) p))
					++p;
			}
			else
			{
				/* invalid pattern */
			}
			fieldsPresent = (strncmp(p, ";\"", 2) == 0);
			*p = '\0';
			if (fieldsPresent)
				parseExtensionFields(file, entry, p + 2);
		}
	}
	if (entry->fields.count > 0)
		entry->fields.list = file->fields.list;
	for (i = entry->fields.count; i < file->fields.max; ++i)
	{
		file->fields.list[i].key = NULL;
		file->fields.list[i].value = NULL;
	}
}

static char *duplicate(const char *str)
{
	char *result = NULL;
	if (str != NULL)
	{
		result = (char *) malloc(strlen(str) + 1);
		if (result == NULL)
			perror(NULL);
		else
			strcpy(result, str);
	}
	return result;
}

static void readPseudoTags(tagFile * const file, tagFileInfo * const info)
{
	fpos_t startOfLine;
	const size_t prefixLength = strlen(PseudoTagPrefix);
	if (info != NULL)
	{
		info->file.format = 1;
		info->file.sort = TAG_UNSORTED;
		info->program.author = NULL;
		info->program.name = NULL;
		info->program.url = NULL;
		info->program.version = NULL;
	}
	while (1)
	{
		fgetpos(file->fp, &startOfLine);
		if (!readTagLine(file))
			break;
		if (strncmp(file->line.buffer, PseudoTagPrefix, prefixLength) != 0)
			break;
		else
		{
			tagEntry entry;
			const char *key, *value;
			parseTagLine(file, &entry);
			key = entry.name + prefixLength;
			value = entry.file;
			if (strcmp(key, "TAG_FILE_SORTED") == 0)
				file->sortMethod = (sortType) atoi(value);
			else if (strcmp(key, "TAG_FILE_FORMAT") == 0)
				file->format = atoi(value);
			else if (strcmp(key, "TAG_PROGRAM_AUTHOR") == 0)
				file->program.author = duplicate(value);
			else if (strcmp(key, "TAG_PROGRAM_NAME") == 0)
				file->program.name = duplicate(value);
			else if (strcmp(key, "TAG_PROGRAM_URL") == 0)
				file->program.url = duplicate(value);
			else if (strcmp(key, "TAG_PROGRAM_VERSION") == 0)
				file->program.version = duplicate(value);
			if (info != NULL)
			{
				info->file.format = file->format;
				info->file.sort = file->sortMethod;
				info->program.author = file->program.author;
				info->program.name = file->program.name;
				info->program.url = file->program.url;
				info->program.version = file->program.version;
			}
		}
	}
	fsetpos(file->fp, &startOfLine);
}

static void gotoFirstLogicalTag(tagFile * const file)
{
	fpos_t startOfLine;
	const size_t prefixLength = strlen(PseudoTagPrefix);
	rewind(file->fp);
	while (1)
	{
		fgetpos(file->fp, &startOfLine);
		if (!readTagLine(file))
			break;
		if (strncmp(file->line.buffer, PseudoTagPrefix, prefixLength) != 0)
			break;
	}
	fsetpos(file->fp, &startOfLine);
}

static tagFile *initialize(const char *const filePath, tagFileInfo * const info)
{
	tagFile *result = (tagFile *) malloc(sizeof(tagFile));
	if (result != NULL)
	{
		memset(result, 0, sizeof(tagFile));
		growString(&result->line);
		growString(&result->name);
		result->fields.max = 20;
		result->fields.list =
		    (tagExtensionField *) malloc(result->fields.max *
		                                 sizeof(tagExtensionField));
		result->fp = fopen(filePath, "r");
		if (result->fp == NULL)
		{
			free(result);
			result = NULL;
			info->status.error_number = errno;
		}
		else
		{
			fseek(result->fp, 0, SEEK_END);
			result->size = ftell(result->fp);
			rewind(result->fp);
			readPseudoTags(result, info);
			info->status.opened = 1;
			result->initialized = 1;
		}
	}
	return result;
}

static void terminate(tagFile * const file)
{
	fclose(file->fp);

	free(file->line.buffer);
	free(file->name.buffer);
	free(file->fields.list);

	if (file->program.author != NULL)
		free(file->program.author);
	if (file->program.name != NULL)
		free(file->program.name);
	if (file->program.url != NULL)
		free(file->program.url);
	if (file->program.version != NULL)
		free(file->program.version);

	memset(file, 0, sizeof(tagFile));

	free(file);
}

static tagResult readNext(tagFile * const file, tagEntry * const entry)
{
	tagResult result = TagFailure;
	if (file == NULL || !file->initialized)
		result = TagFailure;
	else if (!readTagLine(file))
		result = TagFailure;
	else
	{
		if (entry != NULL)
			parseTagLine(file, entry);
		result = TagSuccess;
	}
	return result;
}

static const char *readFieldValue(const tagEntry * const entry,
                                  const char *const key)
{
	const char *result = NULL;
	int i;
	if (strcmp(key, "kind") == 0)
		result = entry->kind;
	else if (strcmp(key, "file") == 0)
		result = EmptyString;
	else
		for (i = 0; i < entry->fields.count && result == NULL; ++i)
			if (strcmp(entry->fields.list[i].key, key) == 0)
				result = entry->fields.list[i].value;
	return result;
}

static int readTagLineSeek(tagFile * const file, const off_t pos)
{
	int result = 0;
	if (fseek(file->fp, pos, SEEK_SET) == 0)
	{
		result = readTagLine(file);	/* read probable partial line */
		if (pos > 0 && result)
			result = readTagLine(file);	/* read complete line */
	}
	return result;
}

static int nameComparison(tagFile * const file)
{
	int result;
	if (file->search.ignorecase)
	{
		if (file->search.partial)
			result = strnuppercmp(file->search.name, file->name.buffer,
			                      file->search.nameLength);
		else
			result = struppercmp(file->search.name, file->name.buffer);
	}
	else
	{
		if (file->search.partial)
			result = strncmp(file->search.name, file->name.buffer,
			                 file->search.nameLength);
		else
			result = strcmp(file->search.name, file->name.buffer);
	}
	return result;
}

static void findFirstNonMatchBefore(tagFile * const file)
{
#define JUMP_BACK 512
	int more_lines;
	int comp;
	off_t start = file->pos;
	off_t pos = start;
	do
	{
		if (pos < (off_t) JUMP_BACK)
			pos = 0;
		else
			pos = pos - JUMP_BACK;
		more_lines = readTagLineSeek(file, pos);
		comp = nameComparison(file);
	}
	while (more_lines && comp == 0 && pos > 0 && pos < start);
}

static tagResult findFirstMatchBefore(tagFile * const file)
{
	tagResult result = TagFailure;
	int more_lines;
	off_t start = file->pos;
	findFirstNonMatchBefore(file);
	do
	{
		more_lines = readTagLine(file);
		if (nameComparison(file) == 0)
			result = TagSuccess;
	}
	while (more_lines && result != TagSuccess && file->pos < start);
	return result;
}

static tagResult findBinary(tagFile * const file)
{
	tagResult result = TagFailure;
	off_t lower_limit = 0;
	off_t upper_limit = file->size;
	off_t last_pos = 0;
	off_t pos = upper_limit / 2;
	while (result != TagSuccess)
	{
		if (!readTagLineSeek(file, pos))
		{
			/* in case we fell off end of file */
			result = findFirstMatchBefore(file);
			break;
		}
		else if (pos == last_pos)
		{
			/* prevent infinite loop if we backed up to beginning of file */
			break;
		}
		else
		{
			const int comp = nameComparison(file);
			last_pos = pos;
			if (comp < 0)
			{
				upper_limit = pos;
				pos = lower_limit + ((upper_limit - lower_limit) / 2);
			}
			else if (comp > 0)
			{
				lower_limit = pos;
				pos = lower_limit + ((upper_limit - lower_limit) / 2);
			}
			else if (pos == 0)
				result = TagSuccess;
			else
				result = findFirstMatchBefore(file);
		}
	}
	return result;
}

static tagResult findSequential(tagFile * const file)
{
	tagResult result = TagFailure;
	if (file->initialized)
	{
		while (result == TagFailure && readTagLine(file))
		{
			if (nameComparison(file) == 0)
				result = TagSuccess;
		}
	}
	return result;
}

static tagResult find(tagFile * const file, tagEntry * const entry,
                      const char *const name, const int options)
{
	tagResult result = TagFailure;
	file->search.name = name;
	file->search.nameLength = strlen(name);
	file->search.partial = (options & TAG_PARTIALMATCH) != 0;
	file->search.ignorecase = (options & TAG_IGNORECASE) != 0;
	fseek(file->fp, 0, SEEK_END);
	file->size = ftell(file->fp);
	rewind(file->fp);
	if ((file->sortMethod == TAG_SORTED && !file->search.ignorecase) ||
	        (file->sortMethod == TAG_FOLDSORTED && file->search.ignorecase))
		result = findBinary(file);
	else
		result = findSequential(file);

	if (result != TagSuccess)
		file->search.pos = file->size;
	else
	{
		file->search.pos = file->pos;
		if (entry != NULL)
			parseTagLine(file, entry);
	}
	return result;
}

static tagResult findNext(tagFile * const file, tagEntry * const entry)
{
	tagResult result = TagFailure;
	if ((file->sortMethod == TAG_SORTED && !file->search.ignorecase) ||
	        (file->sortMethod == TAG_FOLDSORTED && file->search.ignorecase))
	{
		result = tagsNext(file, entry);
		if (result == TagSuccess && nameComparison(file) != 0)
			result = TagFailure;
	}
	else
	{
		result = findSequential(file);
		if (result == TagSuccess && entry != NULL)
			parseTagLine(file, entry);
	}
	return result;
}

/*
*  EXTERNAL INTERFACE
*/

extern tagFile *tagsOpen(const char *const filePath, tagFileInfo * const info)
{
	return initialize(filePath, info);
}

extern tagResult tagsSetSortType(tagFile * const file, const sortType type)
{
	tagResult result = TagFailure;
	if (file != NULL && file->initialized)
	{
		file->sortMethod = type;
		result = TagSuccess;
	}
	return result;
}

extern tagResult tagsFirst(tagFile * const file, tagEntry * const entry)
{
	tagResult result = TagFailure;
	if (file != NULL && file->initialized)
	{
		gotoFirstLogicalTag(file);
		result = readNext(file, entry);
	}
	return result;
}

extern tagResult tagsNext(tagFile * const file, tagEntry * const entry)
{
	tagResult result = TagFailure;
	if (file != NULL && file->initialized)
		result = readNext(file, entry);
	return result;
}

extern const char *tagsField(const tagEntry * const entry, const char *const key)
{
	const char *result = NULL;
	if (entry != NULL)
		result = readFieldValue(entry, key);
	return result;
}

extern tagResult tagsFind(tagFile * const file, tagEntry * const entry,
	                          const char *const name, const int options)
{
	tagResult result = TagFailure;
	if (file != NULL && file->initialized)
		result = find(file, entry, name, options);
	return result;
}

extern tagResult tagsFindNext(tagFile * const file, tagEntry * const entry)
{
	tagResult result = TagFailure;
	if (file != NULL && file->initialized)
		result = findNext(file, entry);
	return result;
}

extern tagResult tagsClose(tagFile * const file)
{
	tagResult result = TagFailure;
	if (file != NULL && file->initialized)
	{
		terminate(file);
		result = TagSuccess;
	}
	return result;
}

/* vi:set tabstop=8 shiftwidth=4: */
